using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml.XPath;
using CriticalSystems.Instrumentation.Configuration;
using CriticalSystems.Instrumentation.TraceWriters;

namespace CriticalSystems.Instrumentation.Internal
{
    public class Trace
    {
        private static readonly ITraceWriterFactory traceWriterFactory;
        private static readonly IXPathNavigable traceWriterSettings;
        [ThreadStatic] private static IList<Exception> exceptions;
        [ThreadStatic] private static int indentLevel;
        private readonly string fullTypeName;
        private readonly ITraceWriter traceWriter;

        static Trace()
        {
            try
            {
                var configuration = InstrumentationConfiguration.Instance;
                var writerElement = configuration.TraceWriter;
                var factoryTypeName = writerElement.Factory;
                if (!string.IsNullOrEmpty(factoryTypeName))
                {
                    traceWriterFactory = CreateTraceWriterFactory(factoryTypeName);
                    if (traceWriterFactory == null)
                    {
                        throw new ConfigurationErrorsException(string.Format(Resource.Culture,
                                                                             Resource.InvalidTraceWriterFactoryType,
                                                                             factoryTypeName));
                    }
                    traceWriterSettings = writerElement.Settings == null ? null : writerElement.Settings.InnerXml;
                }
                else
                {
                    traceWriterFactory = new SystemDiagnosticTraceWriterFactory();
                }
            }
            catch (Exception ex)
            {
                var fallbackTraceWriter = new SystemDiagnosticTraceWriter();
                fallbackTraceWriter.Write(TraceType.Error, 1, null, typeof (Trace).FullName, "Trace", ex.ToString());
                traceWriterFactory = new SystemDiagnosticTraceWriterFactory();
            }
        }

        public Trace(string fullTypeName)
        {
            this.fullTypeName = fullTypeName;
            traceWriter = traceWriterFactory.Create(fullTypeName, traceWriterSettings);
            if (traceWriter.SimpleWriter)
            {
                traceWriter = new SimpleTraceWriterWrapper(traceWriter);
            }
        }

        public TracingFlags Flags
        {
            get { return traceWriter.Flags; }
        }

        private static ITraceWriterFactory CreateTraceWriterFactory(string factoryTypeName)
        {
            var factoryType = Type.GetType(factoryTypeName, false, true);
            if (factoryType == null)
            {
                throw new ConfigurationErrorsException(string.Format(Resource.Culture,
                                                                     Resource.InvalidTraceWriterFactoryType,
                                                                     factoryTypeName));
            }
            var instance = factoryType.Assembly.CreateInstance(factoryType.FullName, false,
                                                               BindingFlags.Instance |
                                                               BindingFlags.Public,
                                                               null, null,
                                                               CultureInfo.InvariantCulture,
                                                               null);
            return instance as ITraceWriterFactory;
        }

        public void In(string method)
        {
            indentLevel++;
            WriteTraceEntry(TraceType.In, method, null, 0);
        }

        public void In(string method, params object[] parameters)
        {
            indentLevel++;
            WriteTraceEntry(TraceType.In, string.Format(method, parameters), null, 0);
        }

        public void Out(string method)
        {
            WriteTraceEntry(TraceType.Out, method, null, 0);
            indentLevel--;
        }

        public void Out(string method, params object[] parameters)
        {
            WriteTraceEntry(TraceType.Out, string.Format(method, parameters), null, 0);
            indentLevel--;
        }

        public void Out(string method, Exception ex)
        {
            if (exceptions == null)
            {
                exceptions = new List<Exception>();
            }
            if (!exceptions.Contains(ex))
            {
                if ((traceWriter.Flags & TracingFlags.TraceError) != 0)
                {
                    WriteException(method, ex);
                }
                exceptions.Add(ex);
            }
            if ((traceWriter.Flags & (TracingFlags.TraceEntryExit | TracingFlags.TraceEntryExitValues)) != 0)
            {
                WriteTraceEntry(TraceType.Out, method, string.Format(" - FAILED ({0})", ex.GetType().Name), 0);
                indentLevel--;
            }
        }

        public void Info(string method, string text, params object[] parameters)
        {
            if ((traceWriter.Flags & TracingFlags.TraceInfo) != 0)
            {
                WriteTraceEntry(TraceType.Info, method, string.Format(text, parameters), 1);
            }
        }

        public void Warning(string method, string text, params object[] parameters)
        {
            if ((traceWriter.Flags & TracingFlags.TraceWarning) != 0)
            {
                WriteTraceEntry(TraceType.Warning, method, string.Format(text, parameters), 1);
            }
        }

        public void Error(string method, string text, params object[] parameters)
        {
            if ((traceWriter.Flags & TracingFlags.TraceError) != 0)
            {
                WriteTraceEntry(TraceType.Error, method, string.Format(text, parameters), 1);
            }
        }

        public void Error(string method, Exception ex)
        {
            if ((traceWriter.Flags & TracingFlags.TraceError) != 0)
            {
                WriteException(method, ex);
            }
        }

        private void WriteException(string method, Exception ex)
        {
            WriteTraceEntry(TraceType.Error, method, string.Format("Exception: {0}", ex.GetType().Name), 1);
            var reader = new StringReader(ex.Message);
            var line = reader.ReadLine();
            WriteTraceEntry(TraceType.Error, method, string.Format("Message: {0}", line), 1);
            do
            {
                line = reader.ReadLine();
                if (line != null)
                {
                    WriteTraceEntry(TraceType.Error, method, string.Format("         {0}", line), 1);
                }
            } while (line != null);
            var exceptionProperties = GetExceptionProperties(ex);
            var properties = new StringBuilder();
            foreach (KeyValuePair<string, object> pair in exceptionProperties)
            {
                if (properties.Length > 0)
                {
                    properties.Append(", ");
                }
                properties.AppendFormat("{0}={1}", pair.Key, pair.Value);
            }
            WriteTraceEntry(TraceType.Error, method, string.Format("Properties: {0}", properties), 1);
            WriteTraceEntry(TraceType.Error, method, string.Format("Source: {0} in {1}", ex.TargetSite, ex.Source), 1);
            reader = new StringReader(ex.StackTrace);
            line = reader.ReadLine();
            WriteTraceEntry(TraceType.Error, method, string.Format("Stack trace: {0}", line), 1);
            do
            {
                line = reader.ReadLine();
                if (line != null)
                {
                    WriteTraceEntry(TraceType.Error, method, string.Format("             {0}", line), 1);
                }
            } while (line != null);
        }

        private static IList<KeyValuePair<string, object>> GetExceptionProperties(Exception ex)
        {
            var properties = ex.GetType().FindMembers(MemberTypes.Property,
                                                      BindingFlags.Public | BindingFlags.Instance,
                                                      delegate(MemberInfo target, object criteria)
                                                          {
                                                              var include = true;
                                                              foreach (string name in (string[]) criteria)
                                                              {
                                                                  if (target.Name == name)
                                                                  {
                                                                      include = false;
                                                                      break;
                                                                  }
                                                              }
                                                              return include;
                                                          },
                                                      new string[]
                                                          {
                                                              "Message", "TargetSite", "StackTrace",
                                                              "InnerException", "HelpLink", "Data", "Source"
                                                          });
            IList<KeyValuePair<string, object>> propertyNamesAndValues = new List<KeyValuePair<string, object>>();
            foreach (MemberInfo property in properties)
            {
                var value = ex.GetType().InvokeMember(property.Name, BindingFlags.GetProperty, null, ex, null);
                var pair = new KeyValuePair<string, object>(property.Name, value);
                propertyNamesAndValues.Add(pair);
            }
            return propertyNamesAndValues;
        }

        private void WriteTraceEntry(TraceType traceType, string method, string text, int extraIndent)
        {
            traceWriter.Write(traceType, indentLevel + extraIndent, Logging.Instance, fullTypeName, method, text);
        }
    }
}